/**
 * Copyright (c) 2011-2017, Johnny Huang(huangzhiyi@gmail.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package heis.jfinal;

import java.io.File;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.ServerSocket;
import java.util.EnumSet;

import javax.servlet.DispatcherType;

import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.session.DefaultSessionCacheFactory;
import org.eclipse.jetty.server.session.FileSessionDataStore;
import org.eclipse.jetty.server.session.SessionCache;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.webapp.WebAppClassLoader;
import org.eclipse.jetty.webapp.WebAppContext;

import com.jfinal.core.Const;
import com.jfinal.kit.FileKit;
import com.jfinal.kit.LogKit;
import com.jfinal.kit.PathKit;
import com.jfinal.kit.StrKit;
import com.jfinal.server.IServer;
import com.jfinal.server.jetty.Scanner;

/**
 * JettyServer is used to config and start jetty web server. Jetty version 9.4.x
 * 
 * @author Johnny Huang
 *
 */
public class HJettyServer implements IServer {

	private String webAppDir;
	private int port;
	private String context;
	private int scanIntervalSeconds;
	private boolean running = false;
	private Server server;
	private WebAppContext webApp;

	private String keyStorePath = null;
	private String keyStorePassword = null;
	private String keyManagerPassword = null;

	HJettyServer(String webAppDir, int port, String context, int scanIntervalSeconds) {
		if (webAppDir == null)
			throw new IllegalStateException("Invalid webAppDir of web server: " + webAppDir);
		if (port < 0 || port > 65536)
			throw new IllegalArgumentException("Invalid port of web server: " + port);
		if (StrKit.isBlank(context))
			throw new IllegalStateException("Invalid context of web server: " + context);

		this.webAppDir = webAppDir;
		this.port = port;
		this.context = context;
		this.scanIntervalSeconds = scanIntervalSeconds;
	}

	HJettyServer(String webAppDir, int port, String context, String keyStorePath, String keyStorePassword,
			String keyManagerPassword) {
		this(webAppDir, port, context, 0);
		this.keyManagerPassword = keyManagerPassword;
		this.keyStorePassword = keyStorePassword;
		this.keyStorePath = keyStorePath;
	}

	public void start() {
		if (!running) {
			try {
				doStart();
			} catch (Exception e) {
				LogKit.error("Start server error",e);
			}
			running = true;
		}
	}

	public void stop() {
		if (running) {
			try {
				webApp.stop();
				server.stop();
			} catch (Exception e) {
				LogKit.error("Stop server error",e);
			}
			running = false;
		}
	}

	private void doStart() {
		if (!available(port))
			throw new IllegalStateException("port: " + port + " already in use!");

		deleteSessionData();

		LogKit.info("Starting JFinal " + Const.JFINAL_VERSION);
		server = new Server();

		// http配置。
		if (null == this.keyStorePath) {
			HttpConfiguration http_config = new HttpConfiguration(); // HTTP
																		// connector
			ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(http_config));
			connector.setReuseAddress(true);
			connector.setIdleTimeout(30000);

			connector.setPort(port);
			server.addConnector(connector);
		} else {
			// https 配置
			HttpConfiguration https_config = new HttpConfiguration();
			https_config.setSecureScheme("https");
			https_config.setSecurePort(port);
			https_config.setOutputBufferSize(32768);
			https_config.addCustomizer(new SecureRequestCustomizer());
			SslContextFactory sslContextFactory = new SslContextFactory();
			sslContextFactory.setKeyStorePath(this.keyStorePath);
			sslContextFactory.setKeyStorePassword(this.keyStorePassword);
			sslContextFactory.setKeyManagerPassword(this.keyManagerPassword);
			ServerConnector httpsConnector = new ServerConnector(server,
					new SslConnectionFactory(sslContextFactory, "http/1.1"), new HttpConnectionFactory(https_config));
			httpsConnector.setPort(port);
			httpsConnector.setIdleTimeout(500000);
			server.addConnector(httpsConnector);
		}

		webApp = new WebAppContext();
		/**
		 * 增加gzip支持
		 */
		FilterHolder fh = new FilterHolder();
		fh.setAsyncSupported(true);
		fh.setClassName("org.eclipse.jetty.servlets.GzipFilter");
		fh.setInitParameter("mimeTypes",
				"text/html,text/plain,text/xml,text/css,text/javascript,application/javascript,image/gif,image/png");
		EnumSet<DispatcherType> set = EnumSet.noneOf(DispatcherType.class);
		set.add(DispatcherType.REQUEST);
		set.add(DispatcherType.FORWARD);
		set.add(DispatcherType.INCLUDE);
		set.add(DispatcherType.ERROR);
		set.add(DispatcherType.ASYNC);
		webApp.addFilter(fh, "/*", set);

		webApp.setContextPath(context);
		webApp.setResourceBase(webAppDir);
		webApp.setMaxFormContentSize(81920000);
		webApp.getInitParams().put("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
		webApp.getInitParams().put("org.eclipse.jetty.servlet.Default.useFileMappedBuffer", "true");
		webApp.getInitParams().put("org.eclipse.jetty.server.Request.maxFormContentSize", "-1");

		persistSession(webApp);

		server.setHandler(webApp);
		changeClassLoader(webApp);

		// configureScanner
		if (scanIntervalSeconds > 0) {
			Scanner scanner = new Scanner(PathKit.getRootClassPath(), scanIntervalSeconds) {
				public void onChange() {
					try {
						LogKit.debug("\nLoading changes ......");
						webApp.stop();
						JFinalClassLoader loader = new JFinalClassLoader(webApp, getClassPath());
						webApp.setClassLoader(loader);
						webApp.start();
						LogKit.debug("Loading complete.");
					} catch (Exception e) {
						LogKit.error("Error reconfiguring/restarting webapp after change in watched files", e);
					}
				}
			};
			LogKit.info("Starting scanner at interval of " + scanIntervalSeconds + " seconds.");
			scanner.start();
		}

		try {
			LogKit.info("Starting web server on port: " + port);
			server.start();
			LogKit.info("Starting Complete. Welcome To The JFinal World :)");
			server.join();
		} catch (Exception e) {
			LogKit.error("Fail to start jetty server", e);
			System.exit(100);
		}
		return;
	}

	@SuppressWarnings("resource")
	private void changeClassLoader(WebAppContext webApp) {
		try {
			String classPath = getClassPath();
			JFinalClassLoader wacl = new JFinalClassLoader(webApp, classPath);
			wacl.addClassPath(classPath);
		} catch (IOException e) {
			LogKit.error("", e);
		}
	}

	private String getClassPath() {
		return System.getProperty("java.class.path");
	}

	private void deleteSessionData() {
		try {
			FileKit.delete(new File(getStoreDir()));
		} catch (Exception e) {
		}
	}

	private String getStoreDir() {
		String storeDir = PathKit.getWebRootPath() + "/../../session_data" + context;
		if ("\\".equals(File.separator))
			storeDir = storeDir.replaceAll("/", "\\\\");
		return storeDir;
	}

	private void persistSession(WebAppContext webApp) {
		String storeDir = getStoreDir();

		DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory();
		SessionCache sm = webApp.getSessionHandler().getSessionCache();
		if (sm == null) {
			sm = cacheFactory.getSessionCache(webApp.getSessionHandler());
		}
		FileSessionDataStore ds = new FileSessionDataStore();
		ds.setStoreDir(new File(storeDir));
		sm.setSessionDataStore(ds);
		webApp.getSessionHandler().setSessionCache(sm);
	}

	private static boolean available(int port) {
		if (port <= 0) {
			throw new IllegalArgumentException("Invalid start port: " + port);
		}

		ServerSocket ss = null;
		DatagramSocket ds = null;
		try {
			ss = new ServerSocket(port);
			ss.setReuseAddress(true);
			ds = new DatagramSocket(port);
			ds.setReuseAddress(true);
			return true;
		} catch (IOException e) {
		} finally {
			if (ds != null) {
				ds.close();
			}

			if (ss != null) {
				try {
					ss.close();
				} catch (IOException e) {
					LogKit.error("", e);
				}
			}
		}
		return false;
	}

	public static final class JFinalClassLoader extends WebAppClassLoader {
		private boolean initialized = false;

		public JFinalClassLoader(WebAppContext context, String classPath) throws IOException {
			super(context);
			if (classPath != null) {
				String[] tokens = classPath.split(String.valueOf(File.pathSeparatorChar));
				for (String entry : tokens) {
					String path = entry;
					if (path.startsWith("-y-") || path.startsWith("-n-")) {
						path = path.substring(3);
					}

					if (entry.startsWith("-n-") == false) {
						super.addClassPath(path);
					}
				}
			}

			initialized = true;
		}

		@SuppressWarnings({ "unchecked", "rawtypes" })
		public Class loadClass(String name) throws ClassNotFoundException {
			try {
				return loadClass(name, false);
			} catch (NoClassDefFoundError e) {
				throw new ClassNotFoundException(name);
			}
		}

		public void addClassPath(String classPath) throws IOException {
			if (initialized) {
				if (!classPath.endsWith("WEB-INF/classes/"))
					return;
			}
			super.addClassPath(classPath);
		}

		public void addJars(Resource jars) {
			if (initialized) {
				return;
			}
			super.addJars(jars);
		}
	}
}
